package com.fsh.lingsp.common.user.service.impl;

import cn.hutool.core.util.StrUtil;
import com.fsh.lingsp.common.common.domain.dto.RequestInfo;
import com.fsh.lingsp.common.common.event.UserBlackEvent;
import com.fsh.lingsp.common.common.event.UserRegisterEvent;
import com.fsh.lingsp.common.common.utils.AssertUtil;
import com.fsh.lingsp.common.common.utils.RequestHolder;
import com.fsh.lingsp.common.common.utils.sensitive.SensitiveWordBs;
import com.fsh.lingsp.common.user.dao.BlackDao;
import com.fsh.lingsp.common.user.dao.ItemConfigDao;
import com.fsh.lingsp.common.user.dao.UserBackpackDao;
import com.fsh.lingsp.common.user.dao.UserDao;
import com.fsh.lingsp.common.user.domain.dto.ItemInfoDTO;
import com.fsh.lingsp.common.user.domain.dto.SummeryInfoDTO;
import com.fsh.lingsp.common.user.domain.entity.Black;
import com.fsh.lingsp.common.user.domain.entity.ItemConfig;
import com.fsh.lingsp.common.user.domain.entity.User;
import com.fsh.lingsp.common.user.domain.entity.UserBackpack;
import com.fsh.lingsp.common.user.domain.enums.BlackTypeEnum;
import com.fsh.lingsp.common.user.domain.enums.ItemEnum;
import com.fsh.lingsp.common.user.domain.enums.ItemTypeEnum;
import com.fsh.lingsp.common.user.domain.vo.req.user.BlackReq;
import com.fsh.lingsp.common.user.domain.vo.req.user.ItemInfoReq;
import com.fsh.lingsp.common.user.domain.vo.req.user.SummeryInfoReq;
import com.fsh.lingsp.common.user.domain.vo.resp.user.BadgeResp;
import com.fsh.lingsp.common.user.domain.vo.req.user.ModifyNameReq;
import com.fsh.lingsp.common.user.domain.vo.resp.user.UserInfoResp;
import com.fsh.lingsp.common.user.service.UserService;
import com.fsh.lingsp.common.user.service.adapter.UserAdapter;
import com.fsh.lingsp.common.user.service.cache.ItemCache;
import com.fsh.lingsp.common.user.service.cache.UserCache;
import com.fsh.lingsp.common.user.service.cache.UserSummaryCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    @Autowired
    private UserBackpackDao userBackpackDao;
    @Autowired
    private ItemCache itemCache;
    @Autowired
    private UserCache userCache;
    @Autowired
    private UserSummaryCache userSummaryCache;
    @Autowired
    private ItemConfigDao itemConfigDao;
    @Autowired
    private BlackDao blackDao;
    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;
    @Autowired
    private SensitiveWordBs sensitiveWordBs;

    /**
     * 用户注册
     */
    @Override
    @Transactional
    public void register(User u) {
        boolean save = userDao.save(u);
        // 用户注册的事件。 用Spring的事件监听器。  this代表是本类发布的事件
        applicationEventPublisher.publishEvent(new UserRegisterEvent(this,u));
    }

    /**
     * 获取用户个人信息
     */
    @Override
    public UserInfoResp getUserInfo() {
        //当前用户
        RequestInfo requestInfo = RequestHolder.get();
        //查询用户信息（做缓存）
        User user = userCache.getUserInfo(requestInfo.getUid());
        //查询用户背包中的改名卡数量。双字段确定uid 和 item_id
        Integer modifyNameChance=userBackpackDao.getCountByUidAndItemId(user.getId(), ItemEnum.MODIFY_NAME_CARD.getId());
        return UserAdapter.buildUserInfoResp(user,modifyNameChance);
    }

    /**
     * 修改用户名
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void modifyName(ModifyNameReq req) {
        //当前用户
        RequestInfo requestInfo = RequestHolder.get();
        //1、首先判断数据库中有没有相同的名字，不能重复
        User user=userDao.getByName(req.getName());
        AssertUtil.isFalse(sensitiveWordBs.hasSensitiveWord(req.getName()), "名字中包含敏感词，请重新输入"); // 判断名字中有没有敏感词
//        if (Objects.nonNull(user)){
//            throw new BusinessException("名字重复了，请换一个");
//        }
        //使用工具类 ， 断言抛异常
        AssertUtil.isEmpty(user,"名字重复了，请换一个吧~");
        //2、该用户必须持有一张改名卡。  持有多张，使用主键id最小的。
        UserBackpack firstReNameCard =userBackpackDao.getFirstItem(requestInfo.getUid(),ItemEnum.MODIFY_NAME_CARD.getId());
        AssertUtil.isNotEmpty(firstReNameCard,"改名卡不够用了，等后续活动送卡吧~");
        //3、使用改名卡。
        Boolean success=userBackpackDao.useItem(firstReNameCard);
        //4、改名卡使用成功后，则修改名字
        if (success){
            userDao.modifyName(requestInfo.getUid(),req.getName());
            //用户信息改变，调用缓存变更的方法--》删除缓存，更新最后一次“更新用户信息的时间”。
            userCache.userInfoChange(requestInfo.getUid());
        }
    }

    /**
     * 可选徽章预览。
     *   1.所有徽章基本是固定的，可以放入本地缓存。
     *   2.个人点亮(拥有)的徽章
     */
    @Override
    public List<BadgeResp> badges(Long uid) {
        //查询所有的徽章并缓存
        List<ItemConfig> itemConfigList = itemCache.getByType(ItemTypeEnum.BADGE.getType());
        //取出所有徽章的id
        List<Long> idList = itemConfigList.stream().map(ItemConfig::getId).collect(Collectors.toList());
        //查询用户拥有的徽章
        List<UserBackpack> userBackpackList = userBackpackDao.getByItemIds(uid, idList);
        //查询用户佩戴的徽章。 就是user表记录中的 item_id 字段
        User user=userDao.getById(uid);
        //封装数据返回
        return UserAdapter.buildBadgeResp(itemConfigList,userBackpackList,user);
    }

    /**
     * 佩戴徽章
     */
    @Override
    public void wearingBadge(Long uid, Long itemId) {
        //确保用户背包中有 itemId 这个物品
        UserBackpack backpack = userBackpackDao.getFirstItem(uid, itemId);
        AssertUtil.isNotEmpty(backpack,"您还没有这个物品，快去获得吧~");
        //确保这个物品是一枚徽章
        ItemConfig itemConfig = itemConfigDao.getById(backpack.getItemId());
        AssertUtil.equal(ItemTypeEnum.BADGE.getType(),itemConfig.getType(),"这个不是徽章哦~只有徽章才能佩戴！");
        //佩戴徽章。 即更新用户的item_id
        userDao.wearingBadge(uid,itemId);
        //用户信息改变，调用缓存变更的方法--》删除缓存，更新最后一次“更新用户信息的时间”。
        userCache.userInfoChange(uid);
    }

    /**
     * 拉黑目标
     */
    @Override
    @Transactional
    public void black(BlackReq req) {
        //构建实体，将目标的uid 添加到黑名单表
        Black blackUid=new Black();
        blackUid.setType(BlackTypeEnum.UID.getType());
        blackUid.setTarget(req.getUid().toString());
        blackDao.save(blackUid);

        //构建实体，将目标的ip（无论创建ip还是更新ip） 添加到黑名单表
        User user = userDao.getById(req.getUid());
        // 创建ip还是更新ip一样的话，那就只需要添加一次。因为我们的数据库中black表添加了唯一索引。
        if (user.getIpInfo().getCreateIp().equals(user.getIpInfo().getUpdateIp())){
            blackIp(user.getIpInfo().getUpdateIp());
        }else{
            blackIp(user.getIpInfo().getCreateIp());
            blackIp(user.getIpInfo().getUpdateIp());
        }

        //注册事件，把拉黑信息返回给前端
        applicationEventPublisher.publishEvent(new UserBlackEvent(this,user));
    }

    //拉黑ip
    private void blackIp(String ip) {
        if (StrUtil.isBlank(ip)){
            return;
        }
        Black blackIp=new Black();
        blackIp.setType(BlackTypeEnum.IP.getType());
        blackIp.setTarget(ip);
        blackDao.save(blackIp);
    }


    /**
     * 获取用户汇总信息。 判断哪些用户是需要更新信息，哪些是不需要更新。
     *
     * @param req  用户信息入参，包含两个属性。1.用户的uid；2.最近一次更新用户信息时间
     */
    @Override
    public List<SummeryInfoDTO> getSummeryUserInfo(SummeryInfoReq req) {
        //需要前端同步的uid
        List<Long> uidList=getNeedSyncUidList(req.getReqList());
        //加载用户信息
        Map<Long, SummeryInfoDTO> batch = userSummaryCache.getBatch(uidList);

        //构建dto集合。  如果 前端传递的uid 是 batch中的一个key ，那么就需要更新。反之不更新。
        List<SummeryInfoDTO> summeryInfoDTOList = req.getReqList().stream()
                .map(infoReq -> batch.containsKey(infoReq.getUid()) ? batch.get(infoReq.getUid()) : SummeryInfoDTO.skip(infoReq.getUid()))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        return summeryInfoDTOList;
    }

    // 获取需要前端同步的uid。 哪些uid的信息需要更新。
    private List<Long> getNeedSyncUidList(List<SummeryInfoReq.infoReq> reqList) {
        // 存放需要同步的uid集合
        List<Long> needSyncUidList=new ArrayList<>();

        // 取出uid集合
        List<Long> list = reqList.stream()
                .map(SummeryInfoReq.infoReq::getUid)
                .collect(Collectors.toList());
        // 从缓存中查每个 uid 的最近一次更新时间。
        List<Long> userModifyTimeList =userCache.getUserModifyTime(list);

        // 遍历 reqList（这是传过来的每个用户的更新时间）
        for (int i = 0; i < reqList.size(); i++) {
            // 获取当前索引位置的 infoReq 中的用户修改时间（前端传递）
            Long lastModifyTime = reqList.get(i).getLastModifyTime();
            // 获取当前索引位置的用户修改时间（缓存中）
            Long modifyTimeCache = userModifyTimeList.get(i);
            // 判断 lastModifyTime 是否为空，或者  modifyTimeCache 是否大于  lastModifyTime
            // 如果满足任一条件，说明该用户的信息需要同步
            if (Objects.isNull(lastModifyTime) ||
                    (Objects.nonNull(modifyTimeCache) && modifyTimeCache > lastModifyTime)) {
                // 将需要同步的用户ID添加到 needSyncUidList 列表中
                needSyncUidList.add(reqList.get(i).getUid());
            }
        }
        return needSyncUidList;
    }

    /**
     * 获取徽章汇总信息。这个仅是针对徽章信息，不和用户产生任何关联，所以没有上面那个难。
     *
     * @param req   徽章信息入参，包含两个属性。1.徽章的id；2.最近一次更新徽章信息时间
     */
    @Override
    public List<ItemInfoDTO> getItemInfo(ItemInfoReq req) {

        //遍历  req.getReqList() ，即遍历每一个徽章。
        List<ItemInfoDTO> list = req.getReqList().stream().map(infoReq -> {
            //缓存中获取
            ItemConfig itemConfig = itemCache.getById(infoReq.getItemId());
            //只要 前端传递的最后一次更新时间不为空，且>=缓存中的时间。那就代表不用同步信息。
            if (Objects.nonNull(infoReq.getLastModifyTime()) && infoReq.getLastModifyTime() >= itemConfig.getUpdateTime().getTime()) {
                return ItemInfoDTO.skip(infoReq.getItemId());
            }

            //同步信息。前端传递的最后一次更新时间不为空，且<缓存中的时间。那就代表需要同步信息。
            ItemInfoDTO dto = new ItemInfoDTO();
            dto.setItemId(itemConfig.getId());
            dto.setImg(itemConfig.getImg());
            dto.setDescribe(itemConfig.getDescribe());
            //needRefresh属性默认为 true，需要刷新。
            return dto;
        }).collect(Collectors.toList());
        return list;
    }
}
